package qc.module.platform.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import qc.common.core.enums.*;
import qc.common.core.exception.QCPromptException;
import qc.common.core.exception.QcTokenInvalidException;
import qc.common.core.json.JsonParserUtil;
import qc.common.core.unify.QCUnifyReturnValue;
import qc.common.core.utils.DateUtil;
import qc.common.core.utils.PasswordUtil;
import qc.module.platform.dto.dept.DeptSimpleDto;
import qc.module.platform.dto.param.value.ObjectParamValuesDto;
import qc.module.platform.dto.project.ProjectDto;
import qc.module.platform.dto.uac.*;
import qc.module.platform.dto.user.UsersDto;
import qc.module.platform.entity.*;
import qc.module.platform.mapper.UserMapper;
import qc.module.platform.repository.QcUserloginLogsRepository;

import java.util.*;
import java.util.stream.Collectors;

/**
 * UserAccessControlService
 *
 * @author QuCheng Tech
 * @create 2023/1/10
 */
@Service
public class UserAccessControlService {
    private QcUserloginLogsRepository repository;
    private UserService userService;
    private UserProjectService userProjectService;
    private ProjectService projectService;
    private UserLoginLogService userLoginLogService;
    private DeptService deptService;
    private RoleService roleService;
    private MenusService menusService;
    private DeptUserService deptUserService;
    private RoleUserService roleUserService;
    private RoleDeptService roleDeptService;
    private RoleMenuService roleMenuService;
    private BlackListService blackListService;

    @Autowired
    public void setQcUserloginLogsRepository(QcUserloginLogsRepository repository) {
        this.repository = repository;
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setUserProjectService(UserProjectService userProjectService) {
        this.userProjectService = userProjectService;
    }

    @Autowired
    public void setProjectService(ProjectService projectService) {
        this.projectService = projectService;
    }

    @Autowired
    public void setUserLoginLogService(UserLoginLogService userLoginLogService) {
        this.userLoginLogService = userLoginLogService;
    }

    @Autowired
    public void setDeptService(DeptService deptService) {
        this.deptService = deptService;
    }

    @Autowired
    public void setRoleService(RoleService roleService) {
        this.roleService = roleService;
    }

    @Autowired
    public void setMenusService(MenusService menusService) {
        this.menusService = menusService;
    }

    @Autowired
    public void setDeptUserService(DeptUserService deptUserService) {
        this.deptUserService = deptUserService;
    }

    @Autowired
    public void setRoleUserService(RoleUserService roleUserService) {
        this.roleUserService = roleUserService;
    }

    @Autowired
    public void setRoleDeptService(RoleDeptService roleDeptService) {
        this.roleDeptService = roleDeptService;
    }

    @Autowired
    public void setRoleMenuService(RoleMenuService roleMenuService) {
        this.roleMenuService = roleMenuService;
    }

    @Autowired
    public void setBlackListService(BlackListService blackListService) {
        this.blackListService = blackListService;
    }

    /**
     * 项目显示信息的json节点名称
     */
    public static final String Project_Display_Info_Node_Name = "displayinfo>*";

    /**
     * 用户登录URL
     */
    public static final String User_Login_URL = "/qc/uac/login";

    /**
     * 用户登录验证处理
     *
     * @param loginData                   登录输入数据
     * @param userAgent                   客户端信息，可以为空
     * @param ip                          客户端IP地址
     * @param token                       用户token，可以为空
     * @param judgeMinutes                判断已有登录失败次数的分钟数，判断最近n分钟失败的次数
     * @param maxAttemptTimes             一个用户允许登录的尝试次数，如果登录失败的次数超过该值不允许进行登录（不加入黑名单）
     * @param addToBlackListMaxTimes      同一个IP地址登录失败次数超过该值将IP地址加入黑名单（不允许该IP地址内的任何用户访问登录接口）
     * @param addToBlackListExpireMinutes 同一个IP地址登录失败次数过多后将IP地址加入黑名单的过期时间，单位：分钟
     * @return qc.module.platform.dto.uac.LoginResultDto
     * @author QuCheng Tech
     * @date 2023/1/11
     */
    public LoginResultDto login(QCAuthValidateMethodEnum validateMethod, LoginDto loginData, String userAgent, String ip, String token,
                                int judgeMinutes, int maxAttemptTimes, int addToBlackListMaxTimes, int addToBlackListExpireMinutes) throws QCPromptException {
        Date nowDate = DateUtil.getNowDate();
        //默认身份验证结果为成功，判断条件错误或验证失败时修改为对应的值
        QCAuthValidateResultEnum validateResult = QCAuthValidateResultEnum.SUCCESS;
        //身份验证处理提示信息，在后面根据提示信息是否为空判断验证是否通过
        StringBuilder prompt = new StringBuilder();

        //根据token或登录信息验证后得到的用户ID和用户信息
        Integer userId = null;
        //根据不同的登录方式得到用户信息，统一进行用户状态和有效性的判断
        QcUsers user = null;
        //QCAuthValidateMethodEnum validateMethod = QCAuthValidateMethodEnum.PWD;
        //获取身份验证方式，需要根据不同的验证方式进行判断；默认验证方式使用账号密码
        //validateMethod = getQCAuthValidateMethodEnum(loginData.getVtype());
        //本次返回的token，如果原token有效使用原token，无效是生成新token
        String returnToken = null;
        //剩余登录尝试次数，相同用户ID的剩余尝试次数；最大允许次数减1（减去本次）
        int remainAttemptTimes = maxAttemptTimes - 1;

        /**
         * 登录验证处理步骤
         * 0.判断当前IP是否在黑名单中，判断当前IP在最近n小时的累计登录失败的次数是否超过最大限制次数
         * 1.判断是否有token
         * 2.有token且有效：如token有效获取对应的用户信息
         * 3.无token或token无效：使用输入的信息获取对应的用户，验证用户名、密码等
         * 4.判断用户状态：禁用、启用时间、过期时间
         * 5.根据输入的项目编码获取项目信息，如未输入项目编码获取用户所在的项目信息集合
         */

        //0.判断当前IP是否在黑名单中，最近n小时累计登录失败的次数
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            if (StringUtils.isNotBlank(ip)) {
                if (blackListService.isInBlackList(ip, User_Login_URL)) {
                    //IP地址在数据库的黑名单中，直接返回；
                    LoginResultDto result = new LoginResultDto();
                    result.setSuccess(false);
                    //因为在黑名单中理论上应该在gateway中已经过滤不进入微服务接口中处理，只有在gateway的黑名单未刷新时会出现该情况，所以此时要直接返回登录结果，不进行登录日志记录
                    //设置登录返回结果中需要刷新网关中的黑名单标记为true
                    result.setRefreshgateway(true);
                    result.setPrompt("当前IP地址在黑名单中，已被禁止访问。");

                    return result;
                } else {
                    //IP地址不在数据库的黑名单中
                    //判断最近n小时累计登录失败次数；判断的n小时与加入黑名单的限制时长相同
                    List<QcUserloginLogs> loginFailRecords = userLoginLogService.getLoginFailTimes(ip, DateUtil.addMinutes(nowDate, -judgeMinutes), nowDate);
                    if (loginFailRecords != null && loginFailRecords.size() >= addToBlackListMaxTimes) {
                        //当前IP地址n分钟内登录失败次数已达到最大值，需要将IP地址列入黑名单
                        //黑名单的过期时间从当前时间开始往后addToBlackListExpireMinutes
                        //只限制登录接口访问，可能用户有其他状态正常的token仍可以继续使用
                        Date blackListExpiredTime = DateUtil.addMinutes(nowDate, addToBlackListExpireMinutes);
                        blackListService.add(ip, User_Login_URL, "当前IP地址已尝试" + loginFailRecords.size() + "次均登录失败，限制用户进行登录操作。", blackListExpiredTime, null);

                        LoginResultDto result = new LoginResultDto();
                        result.setSuccess(false);
                        //设置登录返回结果中需要刷新网关中的黑名单标记为true
                        result.setRefreshgateway(true);
                        result.setPrompt("当前IP地址在" + judgeMinutes + "分钟内已累计登录" + loginFailRecords.size() + "次失败，已限制该IP地址进行用户登录操作。请在" + DateUtil.getDateTimeString(blackListExpiredTime) + "后再重试或联系管理员处理。");

                        return result;
                    }
                }
            }
        }

        //* 1.判断是否有token
        // 特殊情况处理：在验证方式为第三方验证（微信小程序）不管是否传入token及状态，每次调用登录都当做重新登录
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            if (validateMethod != QCAuthValidateMethodEnum.THIRD) {
                if (!StringUtils.isBlank(token)) {
                    //有输入token，根据token获取有效的用户ID
                    //* 2.有token且有效：如token有效获取对应的用户信息
                    try {
                        QcUserloginLogs loginLog = userLoginLogService.getUserLoginLogByToken(token, true);
                        userId = loginLog.getUid();
                    } catch (QcTokenInvalidException exception) {
                        //token无效，此时异常不进行处理
                    }
                    //需要根据uid获取用户信息，判断token是否有效及用户信息是否有效
                    //在根据token获取用户登录记录中已经对登录结果和登录过期时间进行判断，获取到的userId为有效信息，可直接根据id获取用户信息
                    if (userId != null) {
                        user = userService.getEntity(userId);
                        //原token有效，本次返回结果使用原token
                        returnToken = token;
                    }
                }
            }
        }

        //记录当前用户最近n分钟内最早的登录失败时间，用于后面提示可以进行下次登录的时间
        Date userNextAllowLoginTime = null;

        //* 3.无token或token无效：使用输入的信息获取对应的用户，验证用户名、密码等
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            if (userId == null) {
                //根据登录输入信息进行登录身份验证
                if (loginData == null) {
                    validateResult = QCAuthValidateResultEnum.FAIL;
                    prompt.append("登录信息不能为空");
                } else {
                    switch (validateMethod) {
                        case PWD:
                        case SMS://使用短信登录时，code为手机号，pwd为验证码
                            if (StringUtils.isBlank(loginData.getCode())) {
                                validateResult = QCAuthValidateResultEnum.FAIL;
                                prompt.append("登录用户名不能为空");
                            }
                            if (StringUtils.isBlank(loginData.getPwd())) {
                                validateResult = QCAuthValidateResultEnum.FAIL;
                                prompt.append("登录密码不能为空");
                            }
                            break;
                        case THIRD://第三方验证传入身份信息给code
                            if (StringUtils.isBlank(loginData.getCode())) {
                                validateResult = QCAuthValidateResultEnum.FAIL;
                                prompt.append("登录用户名不能为空");
                            }
                            break;
                        default://其他验证类型表示错误
                            validateResult = QCAuthValidateResultEnum.FAIL;
                            prompt.append("用户身份验证方式错误");
                    }
                    if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
                        //如果根据不同身份验证方式通过，才进行用户信息的获取
                        //用户名、密码登录
                        if (validateMethod == QCAuthValidateMethodEnum.PWD) {
                            //根据输入的用户名获取用户信息，用户名可能为登录编码或手机号
                            user = userService.getUserByCodeOrPhone(loginData.getCode());
                        }
                        //短信验证
                        if (validateMethod == QCAuthValidateMethodEnum.SMS) {
                            validateResult = QCAuthValidateResultEnum.FAIL;
                            prompt.append("短信验证方式暂未实现");
                        }
                        //第三方验证
                        if (validateMethod == QCAuthValidateMethodEnum.THIRD) {
                            //未实现第三方验证前，返回验证失败和提示信息
                            //validateResult = QCAuthValidateResultEnum.FAIL;
                            //prompt.append("第三方验证方式暂未实现");
                            //第三方验证的实现思路：根据临时凭据调用对应的第三方接口获取对应的手机号，再根据手机号查询用户信息
                            //String phone = null;
                            //微信验证，根据传入的code（为小程序中获取的临时凭据），调用微信接口换取实际的手机号
                            //调用weixin-miniapp模块中WxMaUserController中的方法
                            //loginData.getCode();
                            //测试使用--模拟直接赋值手机号
                            //phone = "18187120891";

                            String phone = loginData.getCode();

                            //根据手机号查询用户信息
                            user = userService.getUserByPhone(phone);
                        }
                    }

                    //根据输入信息查到后用户信息后将userId进行赋值
                    if (user != null && user.getId() != null) {
                        userId = user.getId();

                        //根据本次登录信息得到有用户信息，判断同一个用户ID已有的登录失败记录是否达到最大次数，只需要确定用户ID相同，不管IP地址是否相同
                        List<QcUserloginLogs> userLoginFailRecords = userLoginLogService.getLoginFailLogsByUserId(userId.intValue(), DateUtil.addMinutes(nowDate, -judgeMinutes), nowDate);
                        if (userLoginFailRecords != null && userLoginFailRecords.size() > 0x0) {
                            //计算剩余尝试次数；在后面根据登录结果判断
                            remainAttemptTimes -= userLoginFailRecords.size();
                            //可能有多个登录失败的记录，需要获取时间最早的登录失败记录中的时间
                            Date latestLoginFailTime = userLoginFailRecords.get(userLoginFailRecords.size() - 0x1).getTm();
                            userNextAllowLoginTime = DateUtil.addMinutes(latestLoginFailTime, judgeMinutes);

                            //如果计算后的剩余次数已经小于0(等于0可能本次是最后1次，本次登录可能成功)，登录失败，提示当前用户已经失败n次，需要在xx时间后重试
                            if (remainAttemptTimes < 0x0) {
                                //当前用户ID已经在n分钟内有超过最大尝试次数的登录失败记录，不允许登录，不进行登录日志记录，直接返回登录失败结果
                                prompt.append("当前用户在" + judgeMinutes + "分钟内已尝试登录" + userLoginFailRecords.size() + "次失败，已限制该用户进行登录操作。请在" + DateUtil.getDateTimeString(userNextAllowLoginTime) + "后再重试。");

                                LoginResultDto result = new LoginResultDto();
                                result.setSuccess(false);
                                //设置登录返回结果中需要刷新网关中的黑名单标记为true
                                result.setRefreshgateway(true);
                                result.setPrompt(prompt.toString());

                                return result;
                            }
                        }
                    }

                    //判断用户是否存在，判断密码或验证码是否正确
                    if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
                        //判断用户信息：是否存在、密码是否正确、是否冻结、是否在有效期
                        if (user == null) {
                            //用户不存在
                            validateResult = QCAuthValidateResultEnum.NOT_EXIST;
                            prompt.append("用户不存在");
                        } else {
                            //验证密码、验证码是否正确
                            if (validateMethod == QCAuthValidateMethodEnum.PWD) {
                                //数据库中存在的密码为加密密码，系统使用6次md5加密
                                if (!loginData.getPwd().equals(user.getPassword())) {
                                    validateResult = QCAuthValidateResultEnum.PWD_ERROR;
                                    prompt.append("密码错误");
                                }
                            } else if (validateMethod == QCAuthValidateMethodEnum.SMS) {
                                //短信验证码登录，还需要在验证码发送记录中进行匹配是否存在，已经手机号和验证码是否对应
                            } else if (validateMethod == QCAuthValidateMethodEnum.THIRD) {
                                //第三方验证不需要进行其他判断，只要前面验证的用户手机号正确即可
                            }
                        }
                    }
                }
            }
        }

        //* 4.判断用户状态：禁用、启用时间、过期时间
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            //验证用户状态
            if (user.getFlag() == ResourceStatusFlagEnum.DISABLE || user.getFlag() == ResourceStatusFlagEnum.DELETED) {
                validateResult = QCAuthValidateResultEnum.USER_LOCKED;
                prompt.append("用户被禁用");
            } else {
                if (user.getAllowtm() != null && DateUtil.getNowDate().before(user.getAllowtm())) {
                    validateResult = QCAuthValidateResultEnum.NOT_VALID;
                    prompt.append("用户允许登录时间未到");
                } else if (user.getExpiredtm() != null && DateUtil.getNowDate().after(user.getExpiredtm())) {
                    validateResult = QCAuthValidateResultEnum.EXPIRED;
                    prompt.append("用户已过期");
                }
            }
        }

        //* 5.根据输入的项目编码获取项目信息，如未输入项目编码获取用户所在的项目信息集合
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            //验证是否在项目中
            //判断是否输入项目编码
            if (loginData == null || StringUtils.isBlank(loginData.getProject())) {
                //未指定项目编码不判定为登录失败，后面根据获取有权限的项目列表进行判断
                //validateResult = QCAuthValidateResultEnum.FAIL;
                //prompt.append("未指定项目编码");
            } else if (userProjectService.hasExist(userId, loginData.getProject()) == false) {
                validateResult = QCAuthValidateResultEnum.NOT_IN_PROJECT;
                prompt.append("用户不在项目中或项目无效");
            }
        } else {
            //如果是用户登录失败，提示剩余次数
            //加上本次登录失败总次数超过单个用户的最大允许登录次数，提示不允许登录
            if (remainAttemptTimes > 0x0)
                prompt.append("，当前用户还允许尝试登录" + remainAttemptTimes + "次，超过该次数仍失败后将不允许该用户进行登录。");
            else
                prompt.append("当前用户在" + judgeMinutes + "分钟内已尝试登录" + maxAttemptTimes + "次失败，已限制该用户进行登录操作。请在" + DateUtil.getDateTimeString(userNextAllowLoginTime) + "后再重试。");
        }

        //获取项目信息，如果传入项目编码获取指定项目信息，如果未传入获取有权限的项目列表
        List<ProjectDto> projectDtoList = new ArrayList<ProjectDto>();
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            //登录验证成功，获取项目信息
            //项目信息，如果指定了项目返回指定项目的信息，如果未指定返回所有项目
            //2025-2-7，修改为不管前端是否指定了项目编码，均返回该用户有权限的所有项目信息集合，以便于前端在登录后可以进行不同项目之间的切换
            //根据用户id获取有权限的项目列表，默认返回第一个
            projectDtoList = projectService.getUserValidProjectList(userId);
            if (projectDtoList == null || projectDtoList.size() < 0x1) {
                validateResult = QCAuthValidateResultEnum.NOT_IN_PROJECT;
                prompt.append("用户不在任何有效项目中");
            } else if (loginData != null && !StringUtils.isBlank(loginData.getProject())) {
                List<ProjectDto> filterByProjectCode = projectDtoList.stream().filter(p -> StringUtils.equalsIgnoreCase(p.getCode(), loginData.getProject())).collect(Collectors.toList());
                if (filterByProjectCode == null || filterByProjectCode.size() < 0x1) {
                    validateResult = QCAuthValidateResultEnum.NOT_IN_PROJECT;
                    prompt.append("指定的项目无效");
                }
            }
        }

        //如果token未传入或无效，生成新的token并作为记录和返回结果
        boolean isNewGenerateToken = false;
        if (validateResult == QCAuthValidateResultEnum.SUCCESS && prompt.length() == 0x0) {
            //如果验证通过才能使用传入的token或生成token
            if (StringUtils.isBlank(returnToken)) {
                returnToken = UUID.randomUUID().toString();
                isNewGenerateToken = true;
            }
        } else {
            //如果验证失败，不生成token，原传入的token也不需要返回
            returnToken = null;
        }

        //判断是否需要修改密码，只需要判断登录验证是否成功
        if (validateResult == QCAuthValidateResultEnum.SUCCESS) {
            //验证密码是否为初始密码
            if (user.getPwdflag() == QcUserPwdSrcTypeEnum.INIT || user.getPwdflag() == QcUserPwdSrcTypeEnum.RESET) {
                //提示信息在已有结果上添加，不覆盖可能已有的提示信息；
                prompt.append("当前密码为初始密码，请修改密码");
            } else {
                //判断密码使用的天数
                Date passwordExpiredDate = DateUtil.addDays(user.getPwdtm(), PasswordUtil.PASSWORD_EXPIRED_DAYS);
                if (passwordExpiredDate.before(DateUtil.getNowDate())) {
                    prompt.append("当前密码已过期，请修改密码");
                }
            }
        }

        //将登录结果保存到登录日志记录，无论验证结果为通过或为不通过均需要进行记录
        //在此可以判断不对重复登录进行记录，token有效时重复登录也重复进行记录，后继根据token查询时按时间降序排序
        QcUserloginLogs userloginLog = new QcUserloginLogs();
        userloginLog.setTm(nowDate);
        //登录的客户端类型枚举
        ClientTypeEnum clienttype = ClientTypeEnum.UN_DEFINE;
        if (loginData != null && loginData.getCtype() != null)
            clienttype = getClientTypeEnum(loginData.getCtype());
        userloginLog.setClienttype(clienttype);
        //前端调用时如没有传入客户端信息，从request的header中获取客户端信息并在此进行赋值
        if (!StringUtils.isBlank(userAgent))
            userloginLog.setClientinfo(userAgent);
        //如果前端调用传入，使用下面的代码对客户端信息进行赋值
        //if (loginData != null && !StringUtils.isBlank(loginData.getCinfo()))
        //    logs.setClientinfo(loginData.getCinfo());
        //设置登录客户端的IP地址或主机名
        if (!StringUtils.isBlank(ip))
            userloginLog.setIp(ip);

        userloginLog.setLogintype(validateMethod);
        if (loginData != null && !StringUtils.isBlank(loginData.getCode()))
            userloginLog.setUcode(loginData.getCode());
        if (loginData != null && !StringUtils.isBlank(loginData.getPwd()))
            userloginLog.setPwd(loginData.getPwd());
        userloginLog.setStatus(validateResult);
        if (prompt.length() > 0x0)
            userloginLog.setMsg(prompt.toString());
        userloginLog.setUid(userId);
        //如果根据登录名得到的有用户信息，记录对应的用户名称
        if (userId != null && user != null)
            userloginLog.setUsername(user.getName());
        //获取登录的项目编码，先从登录传入参数中获取，如果没有指定返回默认第1个
        if (loginData != null && !StringUtils.isBlank(loginData.getProject()))
            userloginLog.setProject(loginData.getProject());
        else if (projectDtoList != null && projectDtoList.size() > 0x0) {
            userloginLog.setProject(projectDtoList.get(0x0).getCode());
        }
        userloginLog.setToken(returnToken);
        //token过期时间根据客户端类型进行计算
        //如果登录结果不为成功或者登录的token为空，默认过期时间为当前时间
        if (validateResult != QCAuthValidateResultEnum.SUCCESS || StringUtils.isBlank(returnToken)) {
            userloginLog.setExpiredtm(nowDate);
        } else {
            if (clienttype == ClientTypeEnum.MOBILE) {
                //移动端设置过期时间为1个月
                userloginLog.setExpiredtm(DateUtil.addMonths(nowDate, 1));
            } else {
                //电脑端设置过期时间为7天
                userloginLog.setExpiredtm(DateUtil.addDays(nowDate, 7));
            }
        }
        userLoginLogService.addLog(userloginLog);

        //登录日志记录添加成功后，对当面用户的同类型客户端已有的token设置过期
        if (userId != null) {
            //只要有获取到对应的用户ID，不管是否登录成功均设置已有的token过期；
            //需要排除返回的token，可能返回的token使用的是已有的token，不重复生成
            userLoginLogService.setTokenExpired(userId, clienttype, nowDate, returnToken);
        }

        //返回登录结果信息
        LoginResultDto result = new LoginResultDto();
        result.setSuccess(validateResult == QCAuthValidateResultEnum.SUCCESS);
        if (prompt.length() > 0x0)
            result.setPrompt(prompt.toString());
        //token
        result.setToken(returnToken);

        //用户信息
        if (user != null)
            result.setName(user.getName());
        //不能将用户信息全部返回给前端，会导致用户信息泄露，不能返回用于显示的名称
        //result.setUser(UserMapper.MAPPER.toDto(user));

        //登录验证成功，返回项目信息
        if (projectDtoList != null && projectDtoList.size() > 0x0) {
            //设置默认项目编码
            result.setProjectcode(projectDtoList.get(0x0).getCode());
            //如果前端传入了项目编码使用前端传入的项目编码
            if (loginData != null && !StringUtils.isBlank(loginData.getProject()))
                result.setProjectcode(loginData.getProject());

            //设置用户有权限的项目列表
            List<ObjectParamValuesDto> projectObjects = new ArrayList<>();
            for (ProjectDto projectDto : projectDtoList) {
                ObjectParamValuesDto projectObj = new ObjectParamValuesDto();

                projectObj.setObjcode(projectDto.getCode());
                projectObj.setObjname(projectDto.getName());
                //固定解析项目的显示信息节点
                if (StringUtils.isNotBlank(projectDto.getParams())) {
                    try {
                        projectObj.setValues(JsonParserUtil.getKeyValues(projectDto.getParams(), Project_Display_Info_Node_Name));
                    } catch (Exception ex) {
                        System.out.println("UserAccessControlService 项目显示信息参数，解析JSON字符串异常，json= " + projectDto.getParams());
                        System.out.println("UserAccessControlService 项目显示信息参数，解析JSON字符串异常，ex=" + ex.toString());
                    }
                }

                projectObjects.add(projectObj);
            }
            result.setProjects(projectObjects);
        }

        return result;
    }

    /**
     * 根据token在登录日志中获取用户信息，返回的项目编码先根据请求头中指定的项目编码，为空时返回登录日志中的项目编码
     *
     * @param token       用户登录凭据
     * @param projectCode 指定的项目编码，可以为空
     * @return qc.module.platform.dto.uac.UacUserInfoDto
     * @author QuCheng Tech
     * @date 2023/2/8
     */
    public UacUserInfoDto getUacUserInfoByToken(String token, String projectCode) throws QcTokenInvalidException {
        //有输入token，根据token获取有效的用户ID
        QcUserloginLogs loginLog = userLoginLogService.getUserLoginLogByToken(token, true);
        //需要根据uid获取用户信息，判断token是否有效及用户信息是否有效
        //在根据token获取用户登录记录中已经对登录结果和登录过期时间进行判断，获取到的userId为有效信息，可直接根据id获取用户信息
        QcUsers user = userService.getEntity(loginLog.getUid());
        //项目编码需要从登录日志中获取
        if (user != null) {
            UacUserInfoDto dto = UserMapper.MAPPER.toUacUserInfoDto(user);
            //如果指定了项目编码返回指定的项目编码，否则使用登录日志中的项目编码
            if (StringUtils.isBlank(projectCode))
                projectCode = loginLog.getProject();

            dto.setProject(projectCode);

            //根据用户ID和项目编码获取用户的部门信息
            try {
                List<DeptSimpleDto> userDepts = deptService.querySimpleInfoList(projectCode, false, user.getId(), false);
                if (userDepts != null && userDepts.size() > 0x0) {
                    DeptSimpleDto firstDeptDto = userDepts.get(0x0);
                    if (firstDeptDto != null) {
                        dto.setDeptId(firstDeptDto.getId());
                        dto.setDeptName(firstDeptDto.getName());
                    }
                }
            } catch (QCPromptException ex) {
                //如果查询用户部门信息报错不进行处理，返回用户信息中没有部门信息即可
            }

            return dto;
        }
        return null;
    }

    /**
     * 根据token在登录日志中获取有效的用户信息
     *
     * @param token 用户凭据
     * @return qc.module.platform.dto.user.UsersDto
     * @author QuCheng Tech
     * @since 2023/8/18
     */
    public UsersDto getUserInfoByToken(String token) throws QcTokenInvalidException {
        int userId = getValidUserId(token);
        QcUsers user = userService.getEntity(userId);
        //项目编码需要从登录日志中获取
        if (user != null)
            return UserMapper.MAPPER.toUserDto(user);

        return null;
    }

    /***
     * 根据token获取有效的用户ID，如果token无效抛出异常提示
     *
     * @param token 用户凭据
     * @return java.lang.Integer
     * @author QuCheng Tech
     * @since 2023/8/18
     */
    public int getValidUserId(String token) throws QcTokenInvalidException {
        QcUserloginLogs loginLog = userLoginLogService.getUserLoginLogByToken(token, true);
        return loginLog.getUid().intValue();
    }

    /**
     * 用户退出登录/注销登录，将指定的token登录记录的过期时间修改为当前时间，不对token和记录进行判断
     *
     * @param token
     * @return java.lang.String
     * @author QuCheng Tech
     * @date 2023/2/8
     */
    public String logout(String token) {
        if (!StringUtils.isBlank(token)) {
            QcUserloginLogs en = new QcUserloginLogs();
            LambdaUpdateWrapper<QcUserloginLogs> wrapper = new LambdaUpdateWrapper<>();
            wrapper.eq(QcUserloginLogs::getToken, token);

            wrapper.set(QcUserloginLogs::getExpiredtm, DateUtil.getNowDate());

            if (repository.update(null, wrapper) < 0x1) {

            }
        }

        return QCUnifyReturnValue.Success();
    }

    /**
     * 用户修改密码
     *
     * @param token 用户登录凭据
     * @param dto   修改密码信息DTO，包含旧密码和新密码
     * @return java.lang.String
     * @author QuCheng Tech
     * @since 2023/8/24
     */
    public String modifyUserPassword(String token, UserModifyPasswordDto dto) throws QCPromptException, QcTokenInvalidException {
        int userId = getValidUserId(token);

        if (dto == null)
            throw new QCPromptException("修改密码输入信息为空");
        if (StringUtils.isBlank(dto.getOldpwd()))
            throw new QCPromptException("旧密码不能为空");
        if (StringUtils.isBlank(dto.getNewpwd()))
            throw new QCPromptException("新密码不能为空");
        if (StringUtils.isBlank(dto.getConfirmpwd()))
            throw new QCPromptException("确认新密码不能为空");
        if (!StringUtils.equals(dto.getNewpwd(), dto.getConfirmpwd()))
            throw new QCPromptException("输入的新密码和确认新密码2次输入的不一致，请重新输入.");
        if (StringUtils.equals(dto.getNewpwd(), dto.getOldpwd()))
            throw new QCPromptException("输入的新密码和旧密码不能相同，请重新输入.");

        //判断旧密码是否正确
        QcUsers user = userService.getEntity(userId);
        //项目编码需要从登录日志中获取
        if (user == null)
            throw new QCPromptException("未获取到有效的用户信息");
        if (!StringUtils.equals(user.getPassword(), dto.getOldpwd()))
            throw new QCPromptException("输入的旧密码不正确，请重新输入.");

        return userService.setPassword(userId, dto.getNewpwd());
    }

    /**
     * 根据token获取用户ID并验证用户状态，用户不存在或状态无效时抛出异常提示信息
     *
     * @param token 用户登录凭据
     * @return java.lang.Integer
     * @author QuCheng Tech
     * @since 2023/8/31
     */
    int getUserIdAndValidate(String token) throws QCPromptException, QcTokenInvalidException {
        int userId = getValidUserId(token);

        //获取用户信息并验证状态
        validateUser(userId);

        return userId;
    }

    /***
     * 获取指定ID的用户信息，并验证状态信息是否有效，无效时抛出异常
     *
     * @param userId 用户ID
     * @return void
     * @author QuCheng Tech
     * @since 2023/8/31
     */
    void validateUser(Integer userId) throws QCPromptException {
        QcUsers user = userService.getEntity(userId);

        if (user == null)
            throw new QCPromptException("指定ID的用户信息不存在，可能用户已删除");

        //验证用户状态
        if (user.getFlag() == ResourceStatusFlagEnum.DISABLE || user.getFlag() == ResourceStatusFlagEnum.DELETED) {
            throw new QCPromptException("用户被禁用");
        } else {
            if (user.getAllowtm() != null && DateUtil.getNowDate().before(user.getAllowtm())) {
                throw new QCPromptException("用户允许登录时间未到");
            } else if (user.getExpiredtm() != null && DateUtil.getNowDate().after(user.getExpiredtm())) {
                throw new QCPromptException("用户已过期");
            }
        }
    }

    /***
     * 根据用户访问凭据token获取指定项目中的有效菜单集合，返回前端路由DTO集合
     *
     * @param token 用户访问凭据
     * @param projectCode 项目编码
     * @return java.util.List<qc.module.platform.dto.uac.UacRouteDto>
     * @author QuCheng Tech
     * @since 2025/2/4
     */
    public List<UacRouteDto> getProjectValidMenus(String token, String projectCode) throws QCPromptException, QcTokenInvalidException {
        //根据token获取用户ID，同时完成用户状态的验证
        int userId = getUserIdAndValidate(token);
        //根据项目编码获取项目信息，项目需要有效
        if (StringUtils.isBlank(projectCode))
            throw new QCPromptException("指定的项目编码为空");
        QcProject project = projectService.getEntity(projectCode, false);
        if (project == null)
            throw new QCPromptException("获取到的项目信息为空，请确认项目编码是否正确");
        if (project.getFlag() != ResourceStatusFlagEnum.NORMAL)
            throw new QCPromptException("获取到的项目状态无效");

        //先获取项目中的所有菜单
        List<QcMenus> projectAllMenus = menusService.getProjectAllMenus(projectCode);
        if (projectAllMenus == null || projectAllMenus.size() < 0x1)
            throw new QCPromptException("当前项目中未配置任何菜单，请先进行菜单配置");
        //筛选过滤的菜单ID集合，如果为空表示所有有效菜单，如果有数据从ID集合中进行筛选
        List<Integer> filterMenuIds = new ArrayList<>();
        //特殊处理：项目在未配置任何角色时返回所有菜单集合
        List<QcRoles> projectAllRoles = roleService.getProjectAllRoles(projectCode, false);
        if (projectAllRoles != null && projectAllRoles.size() > 0x0) {
            //该项目中配置有至少1个角色(不管角色状态是否有效)，根据配置的角色进行权限控制
            //获取状态为有效的角色ID集合，后面需要过滤时使用
            List<Integer> projectAllValidRoleIds = projectAllRoles.stream().filter(p -> p.getFlag() == ResourceStatusFlagEnum.NORMAL).map(p -> p.getId()).collect(Collectors.toList());
            //如果项目中的有效角色为空，不需要进行处理，得到的菜单集合为null
            if (projectAllValidRoleIds == null || projectAllValidRoleIds.size() < 0x1)
                return null;
            //得到角色ID集合：需要同时根据用户与角色关联关系、用户与部门-->部门与角色关联关系得到并集
            List<Integer> roleIds = new ArrayList<>();
            //获取用户关联的角色ID集合
            List<Integer> userRoleIds = roleUserService.getRoleIds(userId, projectAllValidRoleIds);
            if (userRoleIds != null && userRoleIds.size() > 0x0) {
                for (Integer id : userRoleIds) {
                    if (id != null && !roleIds.contains(id))
                        roleIds.add(id);
                }
            }
            //获取用户与部门-->部门与角色关联的角色ID集合
            //需要传入项目中的部门ID作为筛选过滤条件，在此获取项目中的部门信息
            List<QcDepts> userDepts = deptService.getUserDepts(projectCode, userId, true);
            if (userDepts != null && userDepts.size() > 0x0) {
                List<Integer> userDeptIds = userDepts.stream().map(p -> p.getId()).collect(Collectors.toList());
                //根据部门ID集合获取关联的角色ID集合
                List<Integer> deptRoleIds = roleDeptService.getByDeptIds(userDeptIds);
                if (deptRoleIds != null && deptRoleIds.size() > 0x0) {
                    for (Integer id : deptRoleIds) {
                        if (id != null && !roleIds.contains(id))
                            roleIds.add(id);
                    }
                }
            }
            //如果用户和所在部门未关联任何角色，进行异常提示
            if (roleIds == null || roleIds.size() < 0x1)
                throw new QCPromptException("当前用户和所在部门均未配置任何角色，请先进行用户角色配置");
            //根据角色与菜单的关联关系得到全部的菜单ID集合
            List<Integer> menuIds = roleMenuService.getByRoleIds(roleIds);
            filterMenuIds = menuIds;
        } else {
            //如果项目中没有任何1个角色信息，过滤的菜单ID为项目中的所有有效菜单ID
            filterMenuIds = projectAllMenus.stream().filter(p -> p.getFlag() == ResourceStatusFlagEnum.NORMAL).map(p -> p.getId()).collect(Collectors.toList());
        }

        //如果得到的筛选过滤菜单ID集合为空，不需要进行处理，得到的菜单集合为null
        if (filterMenuIds == null || filterMenuIds.size() < 0x1)
            return null;

        //为避免出现下级或子级菜单选中但父级未选中导致的菜单无法显示出来的问题（因为返回菜单是从一级菜单开始遍历获取子级菜单）
        //根据需要显示的菜单将菜单的父级（不管是否与角色关联）均在返回结果中返回
        //根据权限处理得到的菜单ID集合获取菜单信息
        List<Integer> finalFilterMenuIds = filterMenuIds;
        List<QcMenus> filterMenus = projectAllMenus.stream().filter(
                        p -> (finalFilterMenuIds.contains(p.getId())))
                .collect(Collectors.toList());
        for (QcMenus menu : filterMenus) {
            //获取父级菜单
            if (menu.getParentid() != null && menu.getParentid() > 0x0) {
                //上一级菜单ID
                Integer p1MenuId = menu.getParentid();
                //如果权限过滤结果中已有上一级菜单ID，不需要再进行添加
                if (!filterMenuIds.contains(p1MenuId)) {
                    filterMenuIds.add(p1MenuId);
                }
                //上上一级菜单ID
                Optional<QcMenus> p1Menu = projectAllMenus.stream().filter(p -> p.getId() == p1MenuId).findFirst();
                if (p1Menu.isPresent()) {
                    if (p1Menu.get().getParentid() != null && p1Menu.get().getParentid() > 0x0) {
                        Integer p2MenuId = p1Menu.get().getParentid();
                        //如果权限过滤结果中已有上一级菜单ID，不需要再进行添加
                        if (!filterMenuIds.contains(p2MenuId)) {
                            filterMenuIds.add(p2MenuId);
                        }
                        //上上上一级菜单ID
                        Optional<QcMenus> p2Menu = projectAllMenus.stream().filter(p -> p.getId() == p2MenuId).findFirst();
                        if (p2Menu.isPresent()) {
                            if (p2Menu.get().getParentid() != null && p2Menu.get().getParentid() > 0x0) {
                                Integer p3MenuId = p2Menu.get().getParentid();
                                //如果权限过滤结果中已有上一级菜单ID，不需要再进行添加
                                if (!filterMenuIds.contains(p3MenuId)) {
                                    filterMenuIds.add(p3MenuId);
                                }
                                //上上上上一级菜单ID
                                Optional<QcMenus> p3Menu = projectAllMenus.stream().filter(p -> p.getId() == p3MenuId).findFirst();
                                if (p3Menu.isPresent()) {
                                    if (p3Menu.get().getParentid() != null && p3Menu.get().getParentid() > 0x0) {
                                        Integer p4MenuId = p3Menu.get().getParentid();
                                        //如果权限过滤结果中已有上一级菜单ID，不需要再进行添加
                                        if (!filterMenuIds.contains(p4MenuId)) {
                                            filterMenuIds.add(p4MenuId);
                                        }
                                        //已经往上获取4级，暂时不继续往上获取
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        //根据得到的筛选过滤菜单ID进行处理，在ID集合中进行筛选并判断菜单的有效状态
        List<UacRouteDto> result = new ArrayList<>();

        //选取出所有一级菜单节点：
        List<Integer> resultFilterMenuIds = filterMenuIds;
        List<QcMenus> rootMenus = projectAllMenus.stream().filter(
                        p -> (p.getParentid() == null || p.getParentid().equals(0x0))
                                && p.getFlag() == ResourceStatusFlagEnum.NORMAL
                                && resultFilterMenuIds.contains(p.getId())
                )
                .collect(Collectors.toList());
        if (rootMenus != null && rootMenus.size() > 0x0) {
            for (QcMenus lv1Menu : rootMenus) {
                UacRouteDto level1Menu = new UacRouteDto();
                //如果菜单Entity有Name属性，使用Name属性值；否则使用Id属性值
                if (StringUtils.isNotBlank(lv1Menu.getName()))
                    level1Menu.setName(lv1Menu.getName());
                else
                    level1Menu.setName(lv1Menu.getId().toString());
                level1Menu.setPath(lv1Menu.getPath());
                level1Menu.setComponent(lv1Menu.getContent());

                UacRouteMetaDto menuMetaDto = new UacRouteMetaDto();
                menuMetaDto.setIcon(lv1Menu.getIcon());
                menuMetaDto.setTitle(lv1Menu.getTitle());
                if (lv1Menu.getOdr() != null) menuMetaDto.setOrderNo(lv1Menu.getOdr());
                //是否在菜单中显示
                menuMetaDto.setHideMenu(!lv1Menu.getVisiable());

                level1Menu.setMeta(menuMetaDto);

                //判断当前一级菜单节点是否有下级节点
                List<QcMenus> lv2Menus = projectAllMenus.stream().filter(
                        p -> p.getParentid() != null
                                && p.getParentid().equals(lv1Menu.getId())
                                && p.getFlag() == ResourceStatusFlagEnum.NORMAL
                                && resultFilterMenuIds.contains(p.getId())
                ).collect(Collectors.toList());
                if (lv2Menus != null && lv2Menus.size() > 0x0) {
                    List<UacRouteDto> level2Menus = new ArrayList<>();

                    for (QcMenus lv2Menu : lv2Menus) {
                        UacRouteDto level2Menu = new UacRouteDto();
                        //如果菜单Entity有Name属性，使用Name属性值；否则使用Id属性值
                        if (StringUtils.isNotBlank(lv2Menu.getName()))
                            level2Menu.setName(lv2Menu.getName());
                        else
                            level2Menu.setName(lv2Menu.getId().toString());
                        level2Menu.setPath(lv2Menu.getPath());
                        level2Menu.setComponent(lv2Menu.getContent());

                        menuMetaDto = new UacRouteMetaDto();
                        menuMetaDto.setIcon(lv2Menu.getIcon());
                        menuMetaDto.setTitle(lv2Menu.getTitle());
                        if (lv2Menu.getOdr() != null) menuMetaDto.setOrderNo(lv2Menu.getOdr());
                        //是否在菜单中显示
                        menuMetaDto.setHideMenu(!lv2Menu.getVisiable());

                        level2Menu.setMeta(menuMetaDto);

                        //判断当前二级菜单节点是否有下级节点
                        List<QcMenus> lv3Menus = projectAllMenus.stream().filter(
                                p -> p.getParentid() != null
                                        && p.getParentid().equals(lv2Menu.getId())
                                        && p.getFlag() == ResourceStatusFlagEnum.NORMAL
                                        && resultFilterMenuIds.contains(p.getId())
                        ).collect(Collectors.toList());
                        if (lv3Menus != null && lv3Menus.size() > 0x0) {
                            List<UacRouteDto> level3Menus = new ArrayList<>();

                            for (QcMenus lv3Menu : lv3Menus) {
                                UacRouteDto level3Menu = new UacRouteDto();
                                //如果菜单Entity有Name属性，使用Name属性值；否则使用Id属性值
                                if (StringUtils.isNotBlank(lv3Menu.getName()))
                                    level3Menu.setName(lv3Menu.getName());
                                else
                                    level3Menu.setName(lv3Menu.getId().toString());
                                level3Menu.setPath(lv3Menu.getPath());
                                level3Menu.setComponent(lv3Menu.getContent());

                                menuMetaDto = new UacRouteMetaDto();
                                menuMetaDto.setIcon(lv3Menu.getIcon());
                                menuMetaDto.setTitle(lv3Menu.getTitle());
                                if (lv3Menu.getOdr() != null) menuMetaDto.setOrderNo(lv3Menu.getOdr());
                                //是否在菜单中显示
                                menuMetaDto.setHideMenu(!lv3Menu.getVisiable());

                                level3Menu.setMeta(menuMetaDto);

                                level3Menus.add(level3Menu);
                            }

                            level2Menu.setChildren(level3Menus);
                        }

                        level2Menus.add(level2Menu);
                    }

                    level1Menu.setChildren(level2Menus);
                }

                result.add(level1Menu);
            }
        }

        return result;
    }

    /**
     * 根据输入的Integer值获取对应的身份验证方式枚举
     *
     * @param index
     * @return qc.common.core.enums.QCAuthValidateMethodEnum
     * @author QuCheng Tech
     * @date 2023/1/11
     */
    static QCAuthValidateMethodEnum getQCAuthValidateMethodEnum(Integer index) {
        if (index != null && index > 0x0) {
            for (QCAuthValidateMethodEnum item : QCAuthValidateMethodEnum.values()) {
                if (item.getIndex() == index) {//判断序号使用getIndex，判断内容使用getName
                    return item;
                }
            }
        }
        return QCAuthValidateMethodEnum.PWD;
    }

    /**
     * 根据输入的Integer值获取对应的客户端类型枚举
     *
     * @param index
     * @return 客户端类型枚举
     * @author QuCheng Tech
     * @date 2023/1/29
     */
    static ClientTypeEnum getClientTypeEnum(Integer index) {
        if (index != null && index > 0x0) {
            for (ClientTypeEnum item : ClientTypeEnum.values()) {
                if (item.getIndex() == index) {//判断序号使用getIndex，判断内容使用getName
                    return item;
                }
            }
        }
        return ClientTypeEnum.UN_DEFINE;
    }
}
